feat: add slash commands for agent switching#2790
Conversation
Add support for commands that switch the active agent via a new 'agent:' field in the commands section of agent.yaml. Users can now use /plan to switch to a planner sub-agent, /review to switch to a reviewer, etc. Trailing arguments after the command are forwarded to the target agent as the first prompt.
Wires the new agent-switching slash command feature into the built-in coder agent: - Adds a planner sub-agent with read-only tools (filesystem, fetch, todo, user_prompt) - Adds /plan command on root agent to switch to plan mode - Adds symmetric /back command on planner agent to hand work back - Updates root instruction to mention the /plan command
Addresses review feedback from docker-agent bot:
1. In pkg/cli/runner.go (PrepareUserMessage):
- Moved ResolveCommand call BEFORE SetCurrentAgent
- This ensures the command is looked up in the original agent's command table
- Prevents raw slash-command strings from being sent to target agents when
the target agent doesn't have the command defined
2. In pkg/tui/handlers.go (handleAgentCommand):
- Added switchSucceeded flag to track whether agent switch was successful
- Only send resolved message if the agent switch succeeded
- Prevents messages from being sent to the wrong agent when switching fails
7df0e5a to
7e3d57d
Compare
|
/review |
This commit addresses all review feedback from PR docker#2790: **Critical fixes (blocking issues):** 1. **Fix SetCurrentAgent error handling in CLI and TUI** (runner.go, handlers.go) - If SetCurrentAgent fails, we now return an empty message instead of proceeding to send the message to the wrong agent - Added checked type assertion in TUI to prevent potential panics - Both CLI and TUI now properly handle agent switch failures 2. **Add unit tests for PrepareUserMessage and agent switching** (runner_test.go) - Added comprehensive tests for agent-switching commands - Tests cover success cases, failure cases, and edge cases - Tests verify that empty messages are returned on switch failures - Tests verify that agent-only commands (no instruction) work correctly **Non-blocking improvements:** 3. **Update agent-schema.json** (agent-schema.json) - Changed description from 'must be reachable from sub-agent graph' to 'must be defined in team configuration' to match runtime behavior - The runtime validates agent existence but doesn't enforce graph reachability 4. **Add explanatory comment for fallback logic** (commands.go) - Added comment explaining why description falls back to instruction - Clarifies the shorthand command definition behavior 5. **Improve code quality** - Fixed unchecked type assertion in TUI handlers - Used slog.WarnContext instead of slog.Warn for proper context propagation - All linters pass, all tests pass Addresses feedback from @aheritier and docker-agent bot.
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🔴 CRITICAL
Agent-switching slash commands are a well-structured addition. The runtime layer, schema, tests, and TUI path are all clean. One real bug found in the non-TUI (CLI) path: when agent switching fails, the error is signaled via an empty session.UserMessage that the caller unconditionally adds to the session and forwards to the LLM, silently corrupting conversation history.
| // to the wrong agent. Return an empty message to signal the error. | ||
| if err := rt.SetCurrentAgent(cmd.Agent); err != nil { | ||
| slog.WarnContext(ctx, "Failed to switch agent for /command", "agent", cmd.Agent, "error", err) | ||
| return session.UserMessage(""), "" |
There was a problem hiding this comment.
[HIGH] Empty message silently added to session and forwarded to LLM on agent-switch failure
PrepareUserMessage returns session.UserMessage("") here to "signal" the error, but the caller in Run (line 107) unconditionally does:
userMsg, attachedPath := PrepareUserMessage(ctx, rt, userInput, cfg.AttachmentPath)
sess.AddMessage(userMsg) // no guard — empty message is added
rt.RunStream(ctx, sess) // LLM receives empty user message in historyThere is no nil/empty check at the call site. The LLM will receive a blank user turn in the conversation history, which can confuse the model and silently corrupt the session. The TUI path (handleAgentCommand) correctly suppresses the message on failure — the CLI path needs the same treatment.
Suggested fix — return error explicitly so callers are forced to handle it:
func PrepareUserMessage(...) (*session.Message, string, error) {
...
if err := rt.SetCurrentAgent(cmd.Agent); err != nil {
slog.WarnContext(ctx, "Failed to switch agent for /command", ...)
return nil, "", fmt.Errorf("switch agent %q: %w", cmd.Agent, err)
}
...
return msg, attachPath, nil
}Then the caller can continue / surface the error instead of forwarding an empty message.
Summary
Adds support for slash commands that switch the active agent. For example, declaring
/planin the agent config makes the planner sub-agent take over the conversation when the user types/plan.Usage
A new
agent:field can be set on a command inagent.yaml:When the user types
/plan, the active agent is switched toplanner. Anything typed after the command (e.g./plan add a logout button) is forwarded to the target agent as the first user message.agent:can be combined withinstruction:to switch and send a fixed prompt; on its own it acts as a pure handoff.A complete example lives in
examples/agent_switching_commands.yaml.Changes
pkg/config/types/commands.go— newAgentfield onCommand; YAML parser accepts theagentkey.pkg/runtime/commands.go— newLookupCommandhelper;ResolveCommandforwards trailing args verbatim for agent-only commands.pkg/app/app.go— exposesApp.LookupCommand.pkg/tui/handlers.go—handleAgentCommandswitches the active agent before sending the resolved message.pkg/cli/runner.go—PrepareUserMessagedoes the same for the non-TUI flow.agent-schema.json— documents the newagentproperty.examples/agent_switching_commands.yaml— full example with/plan,/review,/back.pkg/runtime/commands_test.go— unit tests for the new behavior.Validation
task lintclean.task testpasses for all changed packages.task buildsucceeds.